﻿////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// <copyright>Copyright 2008-2011 Andreas Huber Doenni</copyright>
// Distributed under the GNU General Public License version 2 (GPLv2).
// See accompanying file License.txt or copy at http://phuse.codeplex.com/license.
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

namespace Phuse.Net.Nntp
{
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using System.Diagnostics.CodeAnalysis;
    using System.Globalization;
    using System.IO;
    using System.Linq;
    using System.Text;
    using System.Threading;
    using Microsoft.VisualStudio.TestTools.UnitTesting;

    using Phuse.IO;
    using Phuse.Mime;
    using Phuse.Text;
    using Phuse.UnitTesting;

    /// <summary>Tests the <see cref="Client"/> class with a local fake server.</summary>
    [SuppressMessage("Microsoft.Maintainability", "CA1506:AvoidExcessiveClassCoupling", Justification = "Test code.")]
    [TestClass]
    public sealed class LocalTest : TestBase
    {
        /// <summary>Tests the initial connection.</summary>
        [TestMethod]
        public void ConnectTest()
        {
            ConnectAvailable(true);
            ConnectAvailable(false);
            ConnectUnavailable(400);
            ConnectUnavailable(502);

            using (var server = NntpFakeServerHelper.MakeUnavailableServer(999))
            {
                AssertThrow<UnexpectedResponseException>(
                    () => new Client("localhost", CancellationToken.None).Dispose());
            }

            AssertThrow<IOException>(() => new Client("localhost", CancellationToken.None).Dispose());
        }

        /// <summary>Tests <see cref="ClientBase{T}.Timeout"/>.</summary>
        [SuppressMessage("Microsoft.Globalization", "CA1303:Do not pass literals as localized parameters", MessageId = "System.Console.WriteLine(System.String)", Justification = "Test code, will not be localized.")]
        [TestMethod]
        public void TimeoutTest()
        {
            TestClient(
                (server, client) =>
                {
                    client.Timeout = Random.Next(1000, 8000);
                    int start = Environment.TickCount;
                    AssertThrow<IOException>(() => new GroupCommand("whatever").Execute(client));
                    int duration = Environment.TickCount - start;
                    Assert.IsTrue(duration >= client.Timeout - 300);
                    Console.WriteLine("Timeout difference: " + (duration - client.Timeout) + "ms");
                });
        }

        /// <summary>Tests the <see cref="ClientBase{T}.MinPipelineLength"/> and
        /// <see cref="ClientBase{T}.MaxPipelineLength"/> properties.</summary>
        [TestMethod]
        public void PipelineLengthTest()
        {
            TestClient(
                (server, client) =>
                {
                    int maxLength = Random.Next();
                    int minLength = Random.Next(maxLength + 1);
                    client.MaxPipelineLength = maxLength;
                    client.MinPipelineLength = minLength;
                    Assert.AreEqual(maxLength, client.MaxPipelineLength);
                    Assert.AreEqual(minLength, client.MinPipelineLength);
                    client.MinPipelineLength = maxLength;
                    Assert.AreEqual(maxLength, client.MinPipelineLength);

                    AssertThrow<ArgumentOutOfRangeException>(
                        () => client.MinPipelineLength = -1,
                        () => client.MinPipelineLength = maxLength + 1,
                        () => client.MaxPipelineLength = maxLength - 1);
                    Assert.AreEqual(maxLength, client.MinPipelineLength);
                    Assert.AreEqual(maxLength, client.MaxPipelineLength);
                });
        }

        /// <summary>Tests the case where the server disconnects unexpectedly.</summary>
        [TestMethod]
        public void ServerDisconnectTest()
        {
            TestClient(
                (server, client) =>
                {
                    client.Timeout = 1000;
                    server.Dispose();
                    AssertThrow<IOException>(() => new GroupCommand("whatever").Execute(client));
                });
        }

        /// <summary>Tests the case where the server does not receive what we are sending it.</summary>
        [TestMethod]
        public void ServerStalledTest()
        {
            TestClient(
                (server, client) =>
                {
                    client.MaxPipelineLength = int.MaxValue;
                    client.Timeout = 1000;

                    // Windows & Windows Mobile seem to have OS write buffers <= 48kB. The group command amounts to
                    // exactly 16 bytes, so if we write it 3073 times, we just exceed the buffer
                    for (int i = 0; i < 3073; ++i)
                    {
                        client.EnqueueCommand(new GroupCommand("whatever"));
                    }

                    AssertThrow<IOException>(() => client.GetResponse());
                });
        }

        /// <summary>Tests <see cref="CapabilitiesCommand"/>.</summary>
        [TestMethod]
        public void CapabilitiesCommandTest()
        {
            TestCommand(
                new CapabilitiesCommand(),
                "CAPABILITIES\r\n",
                helper =>
                {
                    helper.AssertThrow(999);

                    string capabilitiesText = GetUniqueString();

                    using (var response =
                        helper.Execute(Format("101 Capability list follows\r\n{0}\r\n.\r\n", capabilitiesText)))
                    {
                        Assert.IsTrue(response.MoveNext());
                        Assert.AreEqual(capabilitiesText, response.Current);
                        Assert.IsFalse(response.MoveNext());
                    }
                });
        }

        /// <summary>Tests <see cref="GroupCommand"/>.</summary>
        [TestMethod]
        public void GroupCommandTest()
        {
            TestStringCtor(str => new GroupCommand(str).ToString());
            var groupName = GetUniqueString();

            TestCommand(
                new GroupCommand(groupName),
                Format("GROUP {0}\r\n", groupName),
                helper =>
                {
                    helper.AssertThrow(411);
                    helper.AssertThrow(999);

                    const string successFormat = "211 {0} {1} {2} {3}\r\n";
                    var articleCount = Random.Next();
                    var lowWaterMark = Random.Next();
                    var highWaterMark = Random.Next();

                    helper.AssertThrow<UnexpectedResponseException>(
                        Format(successFormat, articleCount, lowWaterMark, null, null)); // Not enough arguments
                    helper.AssertThrow<UnexpectedResponseException>(
                        Format(successFormat, articleCount, "whatever", highWaterMark, groupName)); // Invalid argument
                    helper.AssertThrow<UnexpectedResponseException>(
                        Format(successFormat, articleCount, lowWaterMark, ulong.MaxValue, groupName)); // Overflow

                    AssertAreEqual(helper, successFormat, articleCount, lowWaterMark, highWaterMark, groupName);
                    AssertAreEqual(helper, successFormat, articleCount, lowWaterMark, highWaterMark, null);
                });
        }

        /// <summary>Tests <see cref="ListGroupCommand"/>.</summary>
        [TestMethod]
        public void ListGroupCommandTest()
        {
            TestStringCtor(str => new ListGroupCommand(str).ToString());
            var groupName = GetUniqueString();

            Action<ExecuteHelper<ListGroupResponse>> common =
                helper =>
                {
                    helper.AssertThrow(999);

                    const string successFormat = "211 {0} {1} {2} {3}\r\n{4}\r\n{5}\r\n.\r\n";
                    var articleCount = Random.Next();
                    var lowWaterMark = Random.Next();
                    var highWaterMark = Random.Next();
                    var number1 = Random.Next();
                    var number2 = Random.Next();

                    AssertAreEqual(helper, successFormat, articleCount, lowWaterMark, highWaterMark, groupName, number1, number2);
                };

            TestCommand(
                new ListGroupCommand(groupName),
                Format("LISTGROUP {0}\r\n", groupName),
                (helper => helper.AssertThrow(411)) + common);
            TestCommand(new ListGroupCommand(), "LISTGROUP\r\n", (helper => helper.AssertThrow(412)) + common);
        }

        /// <summary>Tests <see cref="LastCommand"/>.</summary>
        [TestMethod]
        public void LastCommandTest()
        {
            Action<ExecuteHelper<LastResponse>> checkErrors = helper =>
            {
                helper.AssertThrow(412);
                helper.AssertThrow(420);
                helper.AssertThrow(422);
            };

            TestCommand(new LastCommand(), "LAST\r\n", checkErrors + this.ArticleIdResponseTest);
        }

        /// <summary>Tests <see cref="NextCommand"/>.</summary>
        [TestMethod]
        public void NextCommandTest()
        {
            Action<ExecuteHelper<NextResponse>> checkErrors = helper =>
            {
                helper.AssertThrow(412);
                helper.AssertThrow(420);
                helper.AssertThrow(421);
                helper.AssertThrow(502);
                helper.AssertThrow(503);
            };

            TestCommand(new NextCommand(), "NEXT\r\n", checkErrors + this.ArticleIdResponseTest);
        }

        /// <summary>Tests <see cref="ArticleCommand"/>.</summary>
        [TestMethod]
        public void ArticleCommandTest()
        {
            ArticleContentResponseTest<ArticleCommand, ArticleResponse>("ARTICLE", 220);
        }

        /// <summary>Tests <see cref="HeadCommand"/>.</summary>
        [TestMethod]
        public void HeadCommandTest()
        {
            ArticleContentResponseTest<HeadCommand, HeadResponse>("HEAD", 221);
        }

        /// <summary>Tests <see cref="BodyCommand"/>.</summary>
        [TestMethod]
        public void BodyCommandTest()
        {
            ArticleContentResponseTest<BodyCommand, BodyResponse>("BODY", 222);
        }

        /// <summary>Tests <see cref="StatCommand"/>.</summary>
        [TestMethod]
        public void StatCommandTest()
        {
            TestStringCtor(str => new StatCommand(str).ToString());
            AssertThrow<ArgumentOutOfRangeException>(() => new StatCommand(0).ToString());

            var messageId = ServerTest.GetUniqueMessageId();
            var number = Random.Next();
            const string OneArgument = "STAT {0}\r\n";

            Action<ExecuteHelper<StatResponse>> checkErrors1 = helper => helper.AssertThrow(430);
            TestCommand(
                new StatCommand(messageId), Format(OneArgument, messageId), checkErrors1 + this.ArticleIdResponseTest);

            Action<ExecuteHelper<StatResponse>> checkErrors2 = helper =>
            {
                helper.AssertThrow(412);
                helper.AssertThrow(423);
            };

            TestCommand(
                new StatCommand(number), Format(OneArgument, number), checkErrors2 + this.ArticleIdResponseTest);

            Action<ExecuteHelper<StatResponse>> checkErrors3 = helper =>
            {
                helper.AssertThrow(412);
                helper.AssertThrow(420);
            };

            TestCommand(new StatCommand(), "STAT\r\n", checkErrors3 + this.ArticleIdResponseTest);
        }

        /// <summary>Tests <see cref="PostCommand"/>.</summary>
        [TestMethod]
        public void PostCommandTest()
        {
            TestClient(
                (server, client) =>
                {
                    AssertThrow<ArgumentNullException>(() => new PostCommand(null).ToString());

                    const string postSuccessResponse = "340 Send article to be posted\r\n";
                    const string postingNotPermittedResponse = "440 Posting not permitted\r\n";
                    const string articleReceivedSuccessResponse = "240 Article received OK\r\n";
                    const string postingFailedResponse = "441 Posting failed\r\n";

                    var originalRandom = MakeArticle(GetRandomString(512) + "\r\n." + GetRandomString(512));
                    var processedRandom = originalRandom.Replace("\r\n.", "\r\n..") + ".\r\n";
                    SendArticleCallback sendRandomArticle = stream => WriteArticleToStream(stream, originalRandom);

                    server.QueueCommand("POST\r\n", postSuccessResponse);
                    server.QueueCommand(processedRandom, articleReceivedSuccessResponse);
                    new PostCommand(sendRandomArticle).Execute(client);

                    server.QueueCommand("POST\r\n", postingNotPermittedResponse);
                    AssertThrow<PostingNotPermittedException>(() => new PostCommand(sendRandomArticle).Execute(client));

                    server.QueueCommand("POST\r\n", postSuccessResponse);
                    server.QueueCommand(processedRandom, postingFailedResponse);
                    AssertThrow<PostingFailedException>(() => new PostCommand(sendRandomArticle).Execute(client));

                    // Make sure that the buffer in CommandStream overflows once while terminating the command
                    var original = MakeArticle(new string('X', Settings.InMemoryStreamBufferSize - 3));
                    var processed = original.Replace("\r\n.", "\r\n..") + ".\r\n";

                    server.QueueCommand("POST\r\n", postSuccessResponse);
                    server.QueueCommand(processed, articleReceivedSuccessResponse);
                    new PostCommand(stream => WriteArticleToStream(stream, original)).Execute(client);
                });
        }

        /// <summary>Tests <see cref="DateCommand"/>.</summary>
        [TestMethod]
        public void DateCommandTest()
        {
            TestCommand(
                new DateCommand(),
                "DATE\r\n",
                helper =>
                {
                    helper.AssertThrow(999);

                    DateTime now = GetNowWithoutMilliseconds();
                    var response = helper.Execute(Format("111 {0:yyyyMMddHHmmss}\r\n", now));
                    Assert.AreEqual(now, response.Date);
                });
        }

        /// <summary>Tests <see cref="HelpCommand"/>.</summary>
        [TestMethod]
        public void HelpCommandTest()
        {
            TestCommand(
                new HelpCommand(),
                "HELP\r\n",
                helper =>
                {
                    helper.AssertThrow(999);

                    string helpText = GetUniqueString() + "\r\n";
                    Assert.AreEqual(
                        helpText, helper.Execute(Format("100 Help text follows\r\n{0}.\r\n", helpText)).HelpText);
                });
        }

        /// <summary>Tests <see cref="NewNewsCommand"/>.</summary>
        [TestMethod]
        public void NewNewsCommandTest()
        {
            string wildmat = GetUniqueString();
            DateTime dateTime = GetNowWithoutMilliseconds();

            TestCommand(
                new NewNewsCommand(wildmat, dateTime),
                Format("NEWNEWS {0} {1:yyyyMMdd HHmmss} GMT\r\n", wildmat, dateTime),
                helper =>
                {
                    helper.AssertThrow(999);

                    const string successFormat = "230 List of new articles follows\r\n{0}\r\n{1}\r\n.\r\n";
                    string messageId1 = ServerTest.GetUniqueMessageId();
                    string messageId2 = ServerTest.GetUniqueMessageId();

                    using (var response = helper.Execute(Format(successFormat, messageId1, messageId2)))
                    {
                        Assert.IsTrue(response.MoveNext());
                        Assert.AreEqual(messageId1, response.Current);
                        Assert.IsTrue(response.MoveNext());
                        Assert.AreEqual(messageId2, response.Current);
                        Assert.IsFalse(response.MoveNext());
                    }

                    using (var emptyResponse = helper.Execute("230 List of new articles follows\r\n.\r\n"))
                    {
                        Assert.IsFalse(emptyResponse.MoveNext());
                    }
                });
        }

        /// <summary>Tests <see cref="ListActiveCommand"/>.</summary>
        [TestMethod]
        public void ListActiveCommandTest()
        {
            TestCommand(
                new ListActiveCommand(),
                "LIST\r\n",
                helper =>
                {
                    helper.AssertThrow(999);

                    string successFormat = "215 information follows\r\n{0} {1} {2} {3}\r\n{4} {5} {6} {7}\r\n.\r\n";
                    var name1 = GetUniqueString();
                    var highWaterMark1 = Random.Next();
                    var lowWaterMark1 = Random.Next();
                    var name2 = GetUniqueString();
                    var highWaterMark2 = Random.Next();
                    var lowWaterMark2 = Random.Next();

                    // Not enough arguments
                    AssertThrow<UnexpectedResponseException>(() => helper.Execute(Format(
                        successFormat, name1, null, null, null, null, null, null, null)).MoveNext());

                    // Invalid argument type
                    AssertThrow<UnexpectedResponseException>(() => helper.Execute(Format(
                        successFormat, name1, "whatever", null, null, null, null, null, null)).MoveNext());

                    // Argument overflow
                    AssertThrow<UnexpectedResponseException>(() => helper.Execute(Format(
                        successFormat, name1, long.MaxValue, null, null, null, null, null, null)).MoveNext());

                    // First line is ok, but second isn't
                    using (var response1 = helper.Execute(Format(
                        successFormat, name1, highWaterMark1, lowWaterMark1, "y", name2, null, null, null)))
                    {
                        AssertThrow<NotSupportedException>(() => response1.Reset());
                        Assert.IsTrue(response1.MoveNext());
                        AssertAreEqual(name1, highWaterMark1, lowWaterMark1, "y", response1.Current);
                        AssertThrow<UnexpectedResponseException>(() => response1.MoveNext());
                    }

                    // Response with empty lines
                    AssertThrow<UnexpectedResponseException>(
                        () => helper.Execute("215 information follows\r\n\r\n\r\n.\r\n").MoveNext());

                    string successResponse = Format(
                        successFormat, name1, highWaterMark1, lowWaterMark1, "y", name2, highWaterMark2, lowWaterMark2, "n");

                    // Premature disposal
                    var response2 = helper.Execute(successResponse);
                    Assert.IsTrue(response2.MoveNext());
                    response2.Dispose();
                    AssertThrow<ObjectDisposedException>(() => response2.MoveNext());

                    AssertAreEqual(helper, successFormat, name1, highWaterMark1, lowWaterMark1, "y", name2, highWaterMark2, lowWaterMark2, "n");
                    helper.Execute(successResponse); // Exercise unread response
                    AssertAreEqual(helper, successFormat, name1, highWaterMark1, lowWaterMark1, "m", name2, highWaterMark2, lowWaterMark2, "x");
                    AssertAreEqual(helper, successFormat, "." + name1, highWaterMark1, lowWaterMark1, "m", "." + name2, highWaterMark2, lowWaterMark2, "x");
                });
        }

        /// <summary>Tests <see cref="ListNewsgroupsCommand"/>.</summary>
        [TestMethod]
        public void ListNewsgroupsCommandTest()
        {
            Action<ExecuteHelper<ListNewsgroupsResponse>> common =
                helper =>
                {
                    helper.AssertThrow(999);

                    string successFormat = "215 information follows\r\n{0} {1}\r\n{2} {3}\r\n.\r\n";
                    var name1 = GetUniqueString();
                    var description1 = GetUniqueString();
                    var name2 = GetUniqueString();
                    var description2 = GetUniqueString();

                    AssertAreEqual(helper, successFormat, name1, description1, name2, description2);
                    AssertAreEqual(helper, successFormat, name1, null, name2, null);
                };

            TestCommand(new ListNewsgroupsCommand(), "LIST NEWSGROUPS\r\n", common);
            TestStringCtor(str => new ListNewsgroupsCommand(str).ToString());
            var wildmat = GetUniqueString();
            TestCommand(new ListNewsgroupsCommand(wildmat), "LIST NEWSGROUPS " + wildmat + "\r\n", common);
        }

        /// <summary>Tests <see cref="ListOverviewFmtCommand"/>.</summary>
        [TestMethod]
        public void ListOverviewFmtCommandTest()
        {
            TestCommand(
                new ListOverviewFmtCommand(),
                "LIST OVERVIEW.FMT\r\n",
                helper =>
                {
                    helper.AssertThrow(999);

                    string field1 = GetUniqueString();
                    string field2 = GetUniqueString();
                    string field3 = GetUniqueString();

                    using (var response = helper.Execute(
                        Format("215 Information follows\r\n{0}\r\n{1}\r\n{2}\r\n.\r\n", field1, field2, field3)))
                    {
                        Assert.IsTrue(response.MoveNext());
                        Assert.AreEqual(field1, response.Current);
                        Assert.IsTrue(response.MoveNext());
                        Assert.AreEqual(field2, response.Current);
                        Assert.IsTrue(response.MoveNext());
                        Assert.AreEqual(field3, response.Current);
                        Assert.IsFalse(response.MoveNext());
                    }
                });
        }

        /// <summary>Tests <see cref="OverCommand"/>.</summary>
        [TestMethod]
        public void OverCommandTest()
        {
            AssertThrow<ArgumentException>(() => new OverCommand(" ").ToString());
            AssertThrow<ArgumentOutOfRangeException>(
                () => new OverCommand(0).ToString(),
                () => new OverCommand(0, 1).ToString(),
                () => new OverCommand(1, 0).ToString());

            var lowerNumber = Random.Next(int.MaxValue / 2);
            var higherNumber = Random.Next(lowerNumber, int.MaxValue);
            var messageId = GetUniqueString();

            this.TestOverCommand(Format("XOVER {0}\r\n", messageId), new OverCommand(messageId));
            this.TestOverCommand(Format("XOVER {0}\r\n", lowerNumber), new OverCommand(lowerNumber));
            this.TestOverCommand(Format("XOVER {0}-{1}\r\n", lowerNumber, higherNumber), new OverCommand(lowerNumber, higherNumber));
            this.TestOverCommand(Format("XOVER {0}-\r\n", lowerNumber), new OverCommand(lowerNumber, null));
            this.TestOverCommand("XOVER\r\n", new OverCommand());
        }

        /// <summary>Tests <see cref="AuthInfoCommand"/>.</summary>
        [TestMethod]
        public void AuthInfoCommandTest()
        {
            TestClient(
                (server, client) =>
                {
                    AssertThrow<ArgumentException>(
                        () => new AuthInfoCommand(null, "x").ToString(),
                        () => new AuthInfoCommand(string.Empty, "x").ToString(),
                        () => new AuthInfoCommand("x", null).ToString(),
                        () => new AuthInfoCommand("x", string.Empty).ToString());

                    string userName = GetUniqueString();
                    string password = GetUniqueString();
                    string userCommand = Format("AUTHINFO USER {0}\r\n", userName);
                    string passCommand = Format("AUTHINFO PASS {0}\r\n", password);
                    const string successResponse = "281 Authentication accepted\r\n";
                    const string passwordRequiredResponse = "381 Password required\r\n";
                    const string authenticationFailedResponse = "481 Authentication failed\r\n";

                    server.QueueCommand(userCommand, successResponse);
                    new AuthInfoCommand(userName, password).Execute(client);
                    server.QueueCommand(userCommand, passwordRequiredResponse);
                    server.QueueCommand(passCommand, successResponse);
                    new AuthInfoCommand(userName, password).Execute(client);

                    server.QueueCommand(userCommand, passwordRequiredResponse);
                    server.QueueCommand(passCommand, authenticationFailedResponse);
                    AssertThrow<AuthenticationFailedException>(
                        () => new AuthInfoCommand(userName, password).Execute(client));
                });
        }

        /// <summary>Tests the case where the next response is received before the current response has been fully
        /// processed.</summary>
        [TestMethod]
        public void PrematureReceptionOfNextResponseTest()
        {
            TestClient(
                (server, client) =>
                {
                    const string command = "ARTICLE\r\n";
                    const string successFormat = "220 {0} {1}\r\n{2}.\r\n";
                    string content = "MIME-Version: 1.0\r\n\r\n" + GetUniqueString() + "\r\n";
                    string successResponse = Format(successFormat, Random.Next(), ServerTest.GetUniqueMessageId(), content);

                    // Premature reception of next response
                    server.QueueCommand(command, successResponse);
                    server.QueueCommand(command, successResponse);
                    client.EnqueueCommand(new ArticleCommand());
                    client.EnqueueCommand(new ArticleCommand());
                    var response1 = (ArticleResponse)client.GetResponse();
                    response1.Content.ReadByte();
                    var response2 = (ArticleResponse)client.GetResponse();
                    AssertThrow<ObjectDisposedException>(() => response1.Content.ReadByte());
                    response2.Content.ReadByte();
                });
        }

        /// <summary>Tests whether badly behaving commands get the correct exceptions.</summary>
        [TestMethod]
        public void BadCommandTest()
        {
            TestClient(
                (server, client) =>
                {
                    // Are arguments checked?
                    const string command = "MYSUPERCOMMAND\r\n";
                    string[] successResponseCodes = new string[] { "666" };
                    AssertThrow<ArgumentNullException>(
                        () => new BadCommand1(null, successResponseCodes).ToString(),
                        () => new BadCommand1(command, (string[])null).Execute(client).ToString());

                    // Attempt to parse with a type that does not have a Parse method (single-line)
                    var helper1 = new ExecuteHelper<IResponse>(
                        server, () => new BadCommand1(command, successResponseCodes).Execute(client), command);
                    helper1.AssertThrow<InvalidOperationException>("666 whatever 1 2 3\r\n");

                    // Attempt to parse with a type that does not have a Parse method (multi-line)
                    var helper2 = new ExecuteHelper<BadResponse2>(
                        server, () => new BadCommand2(command, successResponseCodes).Execute(client), command);
                    var response2 = helper2.Execute("666\r\nwhatever 5\r\n.\r\n");
                    AssertThrow<InvalidOperationException>(() => response2.MoveNext());
                    AssertThrow<ArgumentNullException>(() => new BadResponse2(null).ToString());

                    // Attempt to pass invalid arguments to create a ResponseStream
                    var helper3 = new ExecuteHelper<IResponse>(
                        server, () => new BadCommand3(command).Execute(client), command);
                    helper3.Execute("666\r\n");

                    AssertThrow<ArgumentOutOfRangeException>(() => new BadResponse3(new MemoryStream()).ToString());
                });
        }

        ////////////////////////////////////////////////////////////////////////////////////////////////////////////////

        private static void AssertAreEqual(
            ExecuteHelper<GroupResponse> helper,
            string successFormat,
            int articleCount,
            int lowWaterMark,
            int highWaterMark,
            string groupName)
        {
            var response = helper.Execute(Format(successFormat, articleCount, lowWaterMark, highWaterMark, groupName));
            Assert.AreEqual(articleCount, response.ArticleCount);
            Assert.AreEqual(lowWaterMark, response.LowWaterMark);
            Assert.AreEqual(highWaterMark, response.HighWaterMark);
            Assert.AreEqual(groupName, response.GroupName);
        }

        private static void AssertAreEqual(
            ExecuteHelper<ListGroupResponse> helper,
            string successFormat,
            int articleCount,
            int lowWaterMark,
            int highWaterMark,
            string groupName,
            int number1,
            int number2)
        {
            var response = helper.Execute(Format(
                successFormat, articleCount, lowWaterMark, highWaterMark, groupName, number1, number2));

            // The current implementation does not parse articleCount, lowWaterMark, highWaterMark, groupName.
            Assert.IsTrue(response.MoveNext());
            Assert.AreEqual(number1, response.Current);
            Assert.IsTrue(response.MoveNext());
            Assert.AreEqual(number2, response.Current);
            Assert.IsFalse(response.MoveNext());
        }

        private void ArticleIdResponseTest<TResponse>(ExecuteHelper<TResponse> helper)
            where TResponse : ArticleIdResponse<TResponse>
        {
            helper.AssertThrow(999);
            AssertAreEqual(helper, "223 {0} {1}\r\n", Random.Next(), ServerTest.GetUniqueMessageId());
        }

        private static void AssertAreEqual<TResponse>(
            ExecuteHelper<TResponse> helper, string successFormat, int number, string messageId)
            where TResponse : ArticleIdResponse<TResponse>
        {
            var response = helper.Execute(Format(successFormat, number, messageId));
            Assert.AreEqual(number, response.Number);
            Assert.AreEqual(messageId, response.MessageId);
        }

        private void ArticleContentResponseTest<TCommand, TResponse>(string command, int successResponseCode)
            where TCommand : CommandBase<Client, TResponse>, new()
            where TResponse : ArticleContentResponse<TResponse>
        {
            TestStringCtor(str => Activator<TCommand>.CreateInstance(str).ToString());
            AssertThrow<ArgumentOutOfRangeException>(() => Activator<TCommand>.CreateInstance(0).ToString());

            var messageId = ServerTest.GetUniqueMessageId();
            var number = Random.Next();

            Action<ExecuteHelper<TResponse>> common =
                helper => ArticleContentResponseCommonTest(helper, successResponseCode, number, messageId);
            Action<ExecuteHelper<TResponse>> check412 = helper => helper.AssertThrow(412);

            string oneArgumentFormat = command + " {0}\r\n";

            TestCommand(
                Activator<TCommand>.CreateInstance(messageId),
                Format(oneArgumentFormat, messageId),
                (helper => helper.AssertThrow(430)) + common);
            TestCommand(
                Activator<TCommand>.CreateInstance(number),
                Format(oneArgumentFormat, number),
                check412 + (helper => helper.AssertThrow(423)) + common);
            TestCommand(new TCommand(), command + "\r\n", check412 + (helper => helper.AssertThrow(420)) + common);
        }

        private static void ArticleContentResponseCommonTest<TResponse>(
            ExecuteHelper<TResponse> helper, int successResponseCode, int number, string messageId)
            where TResponse : ArticleContentResponse<TResponse>
        {
            helper.AssertThrow(999);

            string successFormat = successResponseCode.ToString(CultureInfo.InvariantCulture) + " {0} {1}\r\n{2}.\r\n";
            string content = GetUniqueString() + "\r\n" + GetUniqueString() + "\r\n";
            string successResponse = Format(successFormat, number, messageId, content);

            AssertAreEqual(helper, successFormat, number, messageId, content);
            AssertAreEqual(helper, successFormat, number, null, content);
        }

        private static void AssertAreEqual<TResponse>(
            ExecuteHelper<TResponse> helper, string successFormat, int number, string messageId, string content)
            where TResponse : ArticleContentResponse<TResponse>
        {
            using (var response = helper.Execute(Format(successFormat, number, messageId, content)))
            {
                Assert.AreEqual(number, response.Number);
                Assert.AreEqual(messageId, response.MessageId);
                Assert.AreEqual(content, new StreamReader(response.Content, Encoding.UTF8).ReadToEnd());
            }
        }

        private static void AssertAreEqual(
            ExecuteHelper<ListActiveResponse> helper, string successFormat, string name1, int highWaterMark1, int lowWaterMark1, string status1, string name2, int highWaterMark2, int lowWaterMark2, string status2)
        {
            using (var response = helper.Execute(Format(
                successFormat, DotStuff(name1), highWaterMark1, lowWaterMark1, status1, DotStuff(name2), highWaterMark2, lowWaterMark2, status2)))
            {
                Assert.IsTrue(response.MoveNext());
                AssertAreEqual(name1, highWaterMark1, lowWaterMark1, status1, response.Current);
                Assert.IsTrue(response.MoveNext());
                AssertAreEqual(name2, highWaterMark2, lowWaterMark2, status2, (ActiveGroupInfo)((IEnumerator)response).Current);
                Assert.IsFalse(response.MoveNext());
            }
        }

        private static void AssertAreEqual(
            string name, int highWaterMark, int lowWaterMark, string status, ActiveGroupInfo info)
        {
            Assert.AreEqual(name, info.GroupName);
            Assert.AreEqual(highWaterMark, info.HighWaterMark);
            Assert.AreEqual(lowWaterMark, info.LowWaterMark);
            Assert.AreEqual(GetExpectedStatus(status), info.GroupStatus);
        }

        private void TestOverCommand(string expectedCommand, OverCommand command)
        {
            TestCommand(
                command,
                expectedCommand,
                helper =>
                {
                    helper.AssertThrow(999);

                    const string successFormat =
                        "224 Overview information follows\r\n{0}\t{1}\t{2}\t{3}\t{4}\t{5}\t{6}\t{7}\r\n{8}\t{9}\t{10}\t{11}\t{12}\t{13}\t{14}\t{15}\r\n.\r\n";

                    long number1;
                    string subject1;
                    string from1;
                    string date1;
                    string messageId1;
                    string references1;
                    long bytes1;
                    long lines1;
                    long number2;
                    string subject2;
                    string from2;
                    string date2;
                    string messageId2;
                    string references2;
                    long bytes2;
                    long lines2;

                    SetParameters(out number1, out subject1, out from1, out date1, out messageId1, out references1, out bytes1, out lines1);
                    SetParameters(out number2, out subject2, out from2, out date2, out messageId2, out references2, out bytes2, out lines2);

                    using (var response = helper.Execute(Format(
                        successFormat,
                        number1,
                        subject1,
                        from1,
                        date1,
                        messageId1,
                        references1,
                        bytes1,
                        lines1,
                        number2,
                        subject2,
                        from2,
                        date2,
                        messageId2,
                        references2,
                        bytes2,
                        lines2)))
                    {
                        Assert.IsTrue(response.MoveNext());
                        AssertEqual(number1, subject1, from1, date1, messageId1, references1, bytes1, lines1, response.Current);
                        Assert.IsTrue(response.MoveNext());
                        AssertEqual(number2, subject2, from2, date2, messageId2, references2, bytes2, lines2, response.Current);
                        Assert.IsFalse(response.MoveNext());
                    }

                    using (var emptyResponse = helper.Execute("224 Overview information follows\r\n.\r\n"))
                    {
                        Assert.IsFalse(emptyResponse.MoveNext());
                    }
                });
        }

        private void SetParameters(
            out long number,
            out string subject,
            out string from,
            out string date,
            out string messageId,
            out string references,
            out long bytes,
            out long lines)
        {
            number = Random.Next();
            subject = GetUniqueStringWithSpace();
            from = GetUniqueStringWithSpace();
            date = ToMimeDateTime(DateTime.Now);
            messageId = ServerTest.GetUniqueMessageId();
            references = ServerTest.GetUniqueMessageId() + " " + ServerTest.GetUniqueMessageId();
            bytes = Random.Next();
            lines = Random.Next();
        }

        private static string GetUniqueStringWithSpace()
        {
            return GetUniqueString() + " " + GetUniqueString();
        }

        private void AssertEqual(
            long number,
            string subject,
            string from,
            string date,
            string messageId,
            string references,
            long bytes,
            long lines,
            OverInfo overInfo)
        {
            var message = Message.Parse(overInfo.HeaderOverview);

            Assert.AreEqual(number, overInfo.Number);
            Assert.AreEqual(subject, message.Subject);
            Assert.AreEqual(from, message.From[0].ToString());
            Assert.AreEqual(date, ToMimeDateTime(((DateTime)message.Date).ToLocalTime()));
            Assert.AreEqual(messageId, message.MessageId);
            Assert.AreEqual(references, string.Join(" ", message.References.ToArray()));
            Assert.AreEqual(bytes, overInfo.Bytes);
            Assert.AreEqual(lines, overInfo.Lines);
        }

        private static string ToMimeDateTime(DateTime dateTime)
        {
            return dateTime.ToString("ddd, dd MMM yyyy HH:mm:ss zzz", CultureInfo.InvariantCulture).Remove(29, 1);
        }

        private static string MakeArticle(string raw)
        {
            using (var stream = new MemoryStream())
            using (var fixStream = new FixLineEndingsStream(stream))
            using (var writer = new StreamWriter(fixStream))
            {
                writer.Write(raw);
                writer.Flush();
                var bytes = stream.ToArray();
                return Encoding.UTF8.GetString(bytes, 0, bytes.Length) + "\r\n";
            }
        }

        private static void WriteArticleToStream(Stream stream, string article)
        {
            using (var writer = new StreamWriter(stream))
            {
                writer.Write(article);
            }
        }

        private static GroupStatus GetExpectedStatus(string status)
        {
            switch (status)
            {
                case "y":
                    return GroupStatus.PostingPermitted;
                case "n":
                    return GroupStatus.PostingNotPermitted;
                case "m":
                    return GroupStatus.PostingModerated;
                default:
                    return GroupStatus.Unknown;
            }
        }

        private static void AssertAreEqual(
            ExecuteHelper<ListNewsgroupsResponse> helper,
            string successFormat,
            string name1,
            string description1,
            string name2,
            string description2)
        {
            using (var response = helper.Execute(Format(
                successFormat, DotStuff(name1), description1, DotStuff(name2), description2)))
            {
                Assert.IsTrue(response.MoveNext());
                AssertAreEqual(name1, description1, response.Current);
                Assert.IsTrue(response.MoveNext());
                AssertAreEqual(name2, description2, response.Current);
                Assert.IsFalse(response.MoveNext());
            }
        }

        private static void AssertAreEqual(string name, string description, GroupInfo info)
        {
            Assert.AreEqual(name, info.Name);
            Assert.AreEqual(description, info.Description);
        }

        private static void TestClient(Action<FakeServer, Client> testCallback)
        {
            using (FakeServer server = NntpFakeServerHelper.MakeAvailableServer(false))
            using (Client client = new Client("localhost", CancellationToken.None))
            {
                testCallback(server, client);
            }
        }

        private static void TestCommand<TResponse>(
            CommandBase<Client, TResponse> command,
            string expectedCommand,
            Action<ExecuteHelper<TResponse>> testCallback) where TResponse : IResponse
        {
            TestClient(
                (server, client) =>
                    testCallback(new ExecuteHelper<TResponse>(server, () => command.Execute(client), expectedCommand)));
        }

        private static void ConnectAvailable(bool postingAllowed)
        {
            using (FakeServer server = NntpFakeServerHelper.MakeAvailableServer(postingAllowed))
            {
                Client client;

                using (client = new Client("localhost", CancellationToken.None))
                {
                    Assert.AreEqual(postingAllowed, client.PostingAllowed);
                    AssertThrow<InvalidOperationException>(() => client.GetResponse().ToString());
                    client.EnqueueCommand(new ListActiveCommand());
                    AssertThrow<ArgumentException>(() => new ListActiveCommand().Execute(client));
                }

                Assert.IsFalse(client.PostingAllowed);
                AssertThrow<ObjectDisposedException>(() => client.EnqueueCommand(new ListActiveCommand()));
                AssertThrow<ObjectDisposedException>(() => client.GetResponse());
            }
        }

        private static void ConnectUnavailable(int responseCode)
        {
            using (FakeServer server = NntpFakeServerHelper.MakeUnavailableServer(responseCode))
            {
                AssertThrow<ServiceUnavailableException>(
                    () => new Client("localhost", CancellationToken.None).Dispose());
            }
        }

        private static string DotStuff(string line)
        {
            return ((line != null) && line.StartsWith(".", StringComparison.Ordinal)) ? "." + line : line;
        }

        private static string Format(string format, params object[] args)
        {
            return StringHelper.Format(format, args);
        }

        private static DateTime GetNowWithoutMilliseconds()
        {
            DateTime now = DateTime.UtcNow;
            return new DateTime(now.Year, now.Month, now.Day, now.Hour, now.Minute, now.Second, now.Kind);
        }

        private static void TestStringCtor(Action<string> construct)
        {
            AssertThrow<ArgumentNullException>(() => construct(null));
            AssertThrow<ArgumentException>(
                () => construct(" "), () => construct("\r"), () => construct("\n"), () => construct("\t"));
        }

        /// <summary>Implements a command with a single-line response attempting to parse a type not exposing a Parse
        /// method with the expected signature.</summary>
        private sealed class BadCommand1 : SingleLineCommand<IResponse, object, int, int, int>
        {
            internal BadCommand1(string command, params string[] successResponseCodes) :
                base(command, successResponseCodes)
            {
            }

            /// <summary>See <see cref="SingleLineCommandBase{T, U, V, W, X, Y}.CreateResponse(V, W, X, Y, Stream)"/>.
            /// </summary>
            protected sealed override IResponse CreateResponse(
                object p1, int p2, int p3, int p4, Stream subsequentLines)
            {
                throw new NotImplementedException();
            }
        }

        /// <summary>Implements a command with a multi-line response attempting to parse a type not exposing a Parse
        /// method with the expected signature.</summary>
        private sealed class BadCommand2 : SingleLineCommand<BadResponse2>
        {
            internal BadCommand2(string command, params string[] successResponseCodes) :
                base(command, 2, successResponseCodes)
            {
            }

            /// <summary>See <see cref="SingleLineCommand{T}.IsMultilineResponse"/>.</summary>
            protected sealed override bool IsMultilineResponse(IList<string> initialResponseLineWords)
            {
                return true;
            }

            /// <summary>See <see cref="SingleLineCommandBase{T, U}.CreateResponse"/>.</summary>
            protected sealed override BadResponse2 CreateResponse(ResponseStream responseStream)
            {
                return new BadResponse2(responseStream);
            }
        }

        /// <summary>Implements the response for <see cref="BadCommand2"/>.</summary>
        private sealed class BadResponse2 : MultilineResponse<BadResponse2, IResponse, object, int>
        {
            internal BadResponse2(ResponseStream stream) : base(stream)
            {
            }

            /// <summary>See <see cref="MultilineResponseBase{T, U, V, W}.CreateResponseLine(V, W)"/>.</summary>
            protected sealed override IResponse CreateResponseLine(object p1, int p2)
            {
                throw new NotImplementedException();
            }
        }

        /// <summary>Implements a command that attempts to pass invalid arguments to callbacks.</summary>
        private sealed class BadCommand3 : NonPipelinableCommand<IResponse>
        {
            private readonly byte[] command;

            internal BadCommand3(string command)
            {
                this.command = Encoding.UTF8.GetBytes(command);
            }

            /// <summary>See <see cref="CommandBase{T, U}.SendCommand"/>.</summary>
            [SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods", Justification = "Method is not supposed to be called by external code.")]
            protected sealed override void SendCommand(CreateCommandStreamCallback createCommandStreamCallback)
            {
                using (var stream = createCommandStreamCallback(false))
                {
                    TestBase.AssertThrow<NotSupportedException>(() => stream.ReadByte());
                    stream.Write(this.command, 0, this.command.Length);
                }
            }

            /// <summary>See <see cref="NonPipelinableCommandBase{T, U}.ReceiveResponse"/>.</summary>
            protected sealed override IResponse ReceiveResponse(
                CreateResponseStreamCallback createResponseStreamCallback, CreateCommandStreamCallback createCommandStreamCallback)
            {
                TestBase.AssertThrow<ArgumentNullException>(() => createResponseStreamCallback(null, 2, "666"));
                TestBase.AssertThrow<ArgumentOutOfRangeException>(
                    () => createResponseStreamCallback(Encoding.UTF8, 1, "666"));
                TestBase.AssertThrow<ArgumentNullException>(
                    () => createResponseStreamCallback(Encoding.UTF8, 2, (string[])null));

                using (var stream = createResponseStreamCallback(Encoding.UTF8, 2, "666"))
                {
                    TestBase.AssertThrow<NotSupportedException>(() => stream.WriteByte(42));
                    return null;
                }
            }
        }

        /// <summary>Implements the response for <see cref="BadCommand2"/>.</summary>
        private sealed class BadResponse3 : MultilineResponse<BadResponse3, IResponse>
        {
            /// <summary>See <see cref="MultilineResponseBase{T, U}.CreateResponseLine(ArraySegment{byte}[])"/>.</summary>
            [SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods", Justification = "Method is not supposed to be called by external code.")]
            protected sealed override IResponse CreateResponseLine(ArraySegment<byte>[] responseLineWords)
            {
                return null;
            }

            ////////////////////////////////////////////////////////////////////////////////////////////////////////////////

            internal BadResponse3(Stream subsequentLines) : base(subsequentLines, 0, false, false)
            {
            }
        }
    }
}
